/**!
 * Sortable
 * @author	RubaXa   <trash@rubaxa.org>
 * @author	owenm    <owen23355@gmail.com>
 * @license MIT
 */

import { version } from '../package.json'

import {
	ChromeForAndroid,
	Edge,
	FireFox,
	IE11OrLess,
	IOS,
	Safari
} from './BrowserInfo.js'

import AnimationStateManager from './Animation.js'

import PluginManager from './PluginManager.js'

import dispatchEvent from './EventDispatcher.js'

import {
	clone,
	closest,
	css,
	expando,
	extend,
	find,
	getChild,
	getRect,
	getRelativeScrollOffset,
	getWindowScrollingElement,
	index,
	lastChild,
	matrix,
	off,
	on,
	throttle,
	toggleClass
} from './utils.js'

import {
	fillArray,
	getDirection,
	getElement,
	getPosition,
	initSortableThis
} from './mainUtils.js'

import SwapPlugin from '../plugins/Swap/index.js'

let pluginEvent = function (
	eventName,
	sortable,
	{ evt: originalEvent, ...data } = {}
) {
	PluginManager.pluginEvent.bind(Sortable)(eventName, sortable, {
		dragEl,
		parentEl,
		ghostEl,
		rootEl,
		nextEl,
		lastDownEl,
		cloneEl,
		cloneHidden,
		dragStarted: moved,
		putSortable,
		activeSortable: Sortable.active,
		originalEvent,

		oldIndex,
		oldDraggableIndex,
		newIndex,
		newDraggableIndex,

		hideGhostForTarget: _hideGhostForTarget,
		unhideGhostForTarget: _unhideGhostForTarget,

		cloneNowHidden() {
			cloneHidden = true
		},
		cloneNowShown() {
			cloneHidden = false
		},

		dispatchSortableEvent(name) {
			_dispatchEvent({ sortable, name, originalEvent })
		},

		...data
	})
}

// _dispatchEvent 会通过 new window.CustomEvent 构造一个事件对象，将拖拽元素的信息添加到自定义事件对象上，传递给外部的注册事件函数
function _dispatchEvent(info) {
	// if (oldIndex === undefined || newIndex === undefined) return

	dispatchEvent({
		putSortable,
		cloneEl,
		targetEl: dragEl,
		rootEl,
		oldIndex,
		oldDraggableIndex,
		newIndex,
		newDraggableIndex,
		...info
	})
}

let dragEl,
	parentEl,
	ghostEl,
	rootEl,
	nextEl,
	lastDownEl,
	cloneEl,
	cloneHidden,
	oldIndex,
	newIndex,
	oldDraggableIndex,
	newDraggableIndex,
	activeGroup,
	putSortable,
	awaitingDragStarted = false,
	ignoreNextClick = false,
	sortables = [],
	tapEvt,
	touchEvt,
	lastDx,
	lastDy,
	tapDistanceLeft,
	tapDistanceTop,
	moved,
	lastTarget,
	lastDirection,
	pastFirstInvertThresh = false,
	isCircumstantialInvert = false,
	targetMoveDistance,
	// For positioning ghost absolutely
	ghostRelativeParent,
	ghostRelativeParentInitialScroll = [], // (left, top)
	_silent = false,
	savedInputChecked = [],
	// 最后一个交换的元素
	lastSwapEl

let lastExecutionTime = 0
// 交换灵敏度
const MAX_TRIGGER_TIME = 150

function createVariableCache() {
	let cachedVariables = {
		dragging: false
	} // 用于缓存变量的对象

	// 返回一个函数，用于设置和获取缓存的变量
	return {
		set: function (key, value) {
			cachedVariables[key] = value
		},
		get: function (key) {
			return key ? cachedVariables[key] : cachedVariables
		},
		clear: function () {
			cachedVariables = {}
		}
	}
}

const cacheVar = createVariableCache()
const ISDEV = sessionStorage.getItem('ISDEV')

/** @const */
const documentExists = typeof document !== 'undefined',
	PositionGhostAbsolutely = IOS,
	CSSFloatProperty = Edge || IE11OrLess ? 'cssFloat' : 'float',
	// This will not pass for IE9, because IE9 DnD only works on anchors
	supportDraggable =
		documentExists &&
		!ChromeForAndroid &&
		!IOS &&
		'draggable' in document.createElement('div'),
	supportCssPointerEvents = (function () {
		if (!documentExists) return
		// false when <= IE11
		if (IE11OrLess) {
			return false
		}
		let el = document.createElement('x')
		el.style.cssText = 'pointer-events:auto'
		return el.style.pointerEvents === 'auto'
	})(),
	_detectDirection = function (el, options) {
		let elCSS = css(el),
			elWidth =
				parseInt(elCSS.width) -
				parseInt(elCSS.paddingLeft) -
				parseInt(elCSS.paddingRight) -
				parseInt(elCSS.borderLeftWidth) -
				parseInt(elCSS.borderRightWidth),
			child1 = getChild(el, 0, options),
			child2 = getChild(el, 1, options),
			firstChildCSS = child1 && css(child1),
			secondChildCSS = child2 && css(child2),
			firstChildWidth =
				firstChildCSS &&
				parseInt(firstChildCSS.marginLeft) +
					parseInt(firstChildCSS.marginRight) +
					getRect(child1).width,
			secondChildWidth =
				secondChildCSS &&
				parseInt(secondChildCSS.marginLeft) +
					parseInt(secondChildCSS.marginRight) +
					getRect(child2).width

		if (elCSS.display === 'flex') {
			return elCSS.flexDirection === 'column' ||
				elCSS.flexDirection === 'column-reverse'
				? 'vertical'
				: 'horizontal'
		}

		if (elCSS.display === 'grid') {
			return elCSS.gridTemplateColumns.split(' ').length <= 1
				? 'vertical'
				: 'horizontal'
		}

		if (child1 && firstChildCSS.float && firstChildCSS.float !== 'none') {
			let touchingSideChild2 = firstChildCSS.float === 'left' ? 'left' : 'right'

			return child2 &&
				(secondChildCSS.clear === 'both' ||
					secondChildCSS.clear === touchingSideChild2)
				? 'vertical'
				: 'horizontal'
		}

		return child1 &&
			(firstChildCSS.display === 'block' ||
				firstChildCSS.display === 'flex' ||
				firstChildCSS.display === 'table' ||
				firstChildCSS.display === 'grid' ||
				(firstChildWidth >= elWidth && elCSS[CSSFloatProperty] === 'none') ||
				(child2 &&
					elCSS[CSSFloatProperty] === 'none' &&
					firstChildWidth + secondChildWidth > elWidth))
			? 'vertical'
			: 'horizontal'
	},
	_dragElInRowColumn = function (dragRect, targetRect, vertical) {
		let dragElS1Opp = vertical ? dragRect.left : dragRect.top,
			dragElS2Opp = vertical ? dragRect.right : dragRect.bottom,
			dragElOppLength = vertical ? dragRect.width : dragRect.height,
			targetS1Opp = vertical ? targetRect.left : targetRect.top,
			targetS2Opp = vertical ? targetRect.right : targetRect.bottom,
			targetOppLength = vertical ? targetRect.width : targetRect.height

		return (
			dragElS1Opp === targetS1Opp ||
			dragElS2Opp === targetS2Opp ||
			dragElS1Opp + dragElOppLength / 2 === targetS1Opp + targetOppLength / 2
		)
	},
	/**
	 * Detects first nearest empty sortable to X and Y position using emptyInsertThreshold.
	 * @param  {Number} x      X position
	 * @param  {Number} y      Y position
	 * @return {HTMLElement}   Element of the first found nearest Sortable
	 */
	_detectNearestEmptySortable = function (x, y) {
		let ret
		sortables.some(sortable => {
			const threshold = sortable[expando].options.emptyInsertThreshold
			if (!threshold || lastChild(sortable)) return

			const rect = getRect(sortable),
				insideHorizontally =
					x >= rect.left - threshold && x <= rect.right + threshold,
				insideVertically =
					y >= rect.top - threshold && y <= rect.bottom + threshold

			if (insideHorizontally && insideVertically) {
				return (ret = sortable)
			}
		})
		return ret
	},
	_prepareGroup = function (options) {
		function toFn(value, pull) {
			return function (to, from, dragEl, evt) {
				let sameGroup =
					to.options.group.name &&
					from.options.group.name &&
					to.options.group.name === from.options.group.name

				if (value == null && (pull || sameGroup)) {
					// Default pull value
					// Default pull and put value if same group
					return true
				} else if (value == null || value === false) {
					return false
				} else if (pull && value === 'clone') {
					return value
				} else if (typeof value === 'function') {
					return toFn(value(to, from, dragEl, evt), pull)(to, from, dragEl, evt)
				} else {
					let otherGroup = (pull ? to : from).options.group.name

					return (
						value === true ||
						(typeof value === 'string' && value === otherGroup) ||
						(value.join && value.indexOf(otherGroup) > -1)
					)
				}
			}
		}

		let group = {}
		let originalGroup = options.group

		if (!originalGroup || typeof originalGroup != 'object') {
			originalGroup = { name: originalGroup }
		}

		group.name = originalGroup.name
		group.checkPull = toFn(originalGroup.pull, true)
		group.checkPut = toFn(originalGroup.put)
		group.revertClone = originalGroup.revertClone

		options.group = group
	},
	_hideGhostForTarget = function () {
		if (!supportCssPointerEvents && ghostEl) {
			css(ghostEl, 'display', 'none')
		}
	},
	_unhideGhostForTarget = function () {
		if (!supportCssPointerEvents && ghostEl) {
			css(ghostEl, 'display', '')
		}
	}

// #1184 fix - Prevent click event on fallback if dragged but item not changed position
if (documentExists && !ChromeForAndroid) {
	document.addEventListener(
		'click',
		function (evt) {
			if (ignoreNextClick) {
				evt.preventDefault()
				evt.stopPropagation && evt.stopPropagation()
				evt.stopImmediatePropagation && evt.stopImmediatePropagation()
				ignoreNextClick = false
				return false
			}
		},
		true
	)
}

let nearestEmptyInsertDetectEvent = function (evt) {
	if (dragEl) {
		evt = evt.touches ? evt.touches[0] : evt
		// 传入鼠标在窗口内所在的位置
		let nearest = _detectNearestEmptySortable(evt.clientX, evt.clientY)

		if (nearest) {
			// Create imitation event
			let event = {}
			for (let i in evt) {
				if (evt.hasOwnProperty(i)) {
					event[i] = evt[i]
				}
			}
			event.target = event.rootEl = nearest
			event.preventDefault = void 0
			event.stopPropagation = void 0
			nearest[expando]._onDragOver(event)
		}
	}
}

let _checkOutsideTargetEl = function (evt) {
	if (dragEl) {
		dragEl.parentNode[expando]._isOutsideThisEl(evt.target)
	}
}

/**
 * @class  Sortable
 * @param  {HTMLElement}  el
 * @param  {Object}       [options]
 */
function Sortable(el, options) {
	if (!(el && el.nodeType && el.nodeType === 1)) {
		throw `Sortable: \`el\` must be an HTMLElement, not ${{}.toString.call(el)}`
	}

	this.el = el // root element
	this.options = options = Object.assign({}, options)
	initSortableThis(this.options)

	// Export instance
	el[expando] = this

	let defaults = {
		group: null,
		sort: true,
		disabled: false,
		store: null,
		handle: null,
		draggable: /^[uo]l$/i.test(el.nodeName) ? '>li' : '>*',
		swapThreshold: 1, // percentage; 0 <= x <= 1
		invertSwap: false, // invert always
		invertedSwapThreshold: null, // will be set to same as swapThreshold if default
		removeCloneOnHide: true,
		direction: function () {
			return _detectDirection(el, this.options)
		},
		ghostClass: 'sortable-ghost',
		chosenClass: 'sortable-chosen',
		dragClass: 'sortable-drag',
		ignore: 'a, img',
		filter: null,
		preventOnFilter: true,
		animation: 0,
		easing: null,
		setData: function (dataTransfer, dragEl) {
			dataTransfer.setData('Text', dragEl.textContent)
		},
		dropBubble: false,
		dragoverBubble: false,
		dataIdAttr: 'data-id',
		delay: 0,
		delayOnTouchOnly: false,
		touchStartThreshold:
			(Number.parseInt ? Number : window).parseInt(
				window.devicePixelRatio,
				10
			) || 1,
		forceFallback: false,
		fallbackClass: 'sortable-fallback',
		fallbackOnBody: false,
		fallbackTolerance: 0,
		fallbackOffset: { x: 0, y: 0 },
		supportPointer:
			Sortable.supportPointer !== false && 'PointerEvent' in window && !Safari,
		emptyInsertThreshold: 5
	}

	PluginManager.initializePlugins(this, el, defaults)

	// Set default options
	for (let name in defaults) {
		!(name in options) && (options[name] = defaults[name])
	}

	_prepareGroup(options)

	// Bind all private methods
	for (let fn in this) {
		if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {
			this[fn] = this[fn].bind(this)
		}
	}

	// Setup drag mode
	this.nativeDraggable = options.forceFallback ? false : supportDraggable

	if (this.nativeDraggable) {
		// Touch start threshold cannot be greater than the native dragstart threshold
		this.options.touchStartThreshold = 1
	}

	// Bind events
	if (options.supportPointer) {
		on(el, 'pointerdown', this._onTapStart)
	} else {
		on(el, 'mousedown', this._onTapStart)
		on(el, 'touchstart', this._onTapStart)
	}

	if (this.nativeDraggable) {
		on(el, 'dragover', this)
		on(el, 'dragenter', this)
	}

	sortables.push(this.el)

	// Restore sorting
	options.store && options.store.get && this.sort(options.store.get(this) || [])

	// Add animation state manager
	Object.assign(this, AnimationStateManager())
}

Sortable.prototype = /** @lends Sortable.prototype */ {
	constructor: Sortable,

	_isOutsideThisEl: function (target) {
		if (!this.el.contains(target) && target !== this.el) {
			lastTarget = null
		}
	},

	_getDirection: function (evt, target) {
		return typeof this.options.direction === 'function'
			? this.options.direction.call(this, evt, target, dragEl)
			: this.options.direction
	},

	// 开始拖拽准备事件
	_onTapStart: function (/** Event|TouchEvent */ evt) {
		if (!evt.cancelable) return
		let _this = this,
			el = this.el,
			options = this.options,
			preventOnFilter = options.preventOnFilter,
			type = evt.type,
			touch =
				(evt.touches && evt.touches[0]) ||
				(evt.pointerType && evt.pointerType === 'touch' && evt),
			target = (touch || evt).target,
			originalTarget =
				(evt.target.shadowRoot &&
					((evt.path && evt.path[0]) ||
						(evt.composedPath && evt.composedPath()[0]))) ||
				target,
			filter = options.filter

		// 包含 custom-disabled-class
		if (
			closest(evt.target, options.draggable, el, false) &&
			closest(evt.target, options.draggable, el, false).classList.contains(
				options.customDisabledClass
			)
		) {
			return
		}

		// 保存拖拽输入框内的状态
		_saveInputCheckedState(el)

		// Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group.
		if (dragEl) {
			return
		}

		if (
			(/mousedown|pointerdown/.test(type) && evt.button !== 0) ||
			options.disabled
		) {
			return // only left button and enabled
		}

		// cancel dnd if original target is content editable
		if (originalTarget.isContentEditable) {
			return
		}

		// Safari ignores further event handling after mousedown
		if (
			!this.nativeDraggable &&
			Safari &&
			target &&
			target.tagName.toUpperCase() === 'SELECT'
		) {
			return
		}

		target = closest(target, options.draggable, el, false)

		if (target && target.animated) {
			return
		}

		// 避免重复按下
		if (lastDownEl === target) {
			// Ignoring duplicate `down`
			return
		}

		// Get the index of the dragged element within its parent
		// 获取被拖动元素在其父元素中的索引
		oldIndex = index(target)
		oldDraggableIndex = index(target, options.draggable)
		if (!cacheVar.get('dragging')) {
			cacheVar.set('oldIndex', oldIndex)
			cacheVar.set('oldDraggableIndex', oldDraggableIndex)
		}

		// Check filter
		// ---------------------------暂时用不到不需要看------------------------------
		if (typeof filter === 'function') {
			if (filter.call(this, evt, target, this)) {
				_dispatchEvent({
					sortable: _this,
					rootEl: originalTarget,
					name: 'filter',
					targetEl: target,
					toEl: el,
					fromEl: el
				})
				pluginEvent('filter', _this, { evt })
				preventOnFilter && evt.cancelable && evt.preventDefault()
				return // cancel dnd
			}
		} else if (filter) {
			filter = filter.split(',').some(function (criteria) {
				criteria = closest(originalTarget, criteria.trim(), el, false)

				if (criteria) {
					_dispatchEvent({
						sortable: _this,
						rootEl: criteria,
						name: 'filter',
						targetEl: target,
						fromEl: el,
						toEl: el
					})
					pluginEvent('filter', _this, { evt })
					return true
				}
			})

			if (filter) {
				preventOnFilter && evt.cancelable && evt.preventDefault()
				return // cancel dnd
			}
		}

		// ---------------------------暂时用不到不需要看------------------------------

		// 如果设置了句柄但是按下的不是句柄位置
		if (options.handle && !closest(originalTarget, options.handle, el, false)) {
			return
		}

		// Prepare `dragstart`
		// 准备拖拽
		this._prepareDragStart(evt, touch, target)
	},

	// 上面_onTapStart将所有参数整理出来之后将拖拽的PointerEvent和目标对象dom传入
	_prepareDragStart: function (
		/** Event */ evt,
		/** Touch */ touch,
		/** HTMLElement */ target
	) {
		let _this = this,
			el = _this.el,
			options = _this.options,
			ownerDocument = el.ownerDocument,
			dragStartFn

		/**
		 * target 拖拽对象
		 * dragEl 拖拽的dom
		 * |               target && !dragEl && (target.parentNode === el)                    |
		 * | 如果 拖拽对象存在  并且  dragEl不存在 并且 拖拽对象的父节点全等于拖拽的容器 grid-layout  |
		 */
		if (target && !dragEl && target.parentNode === el) {
			/*
			 * this 整个Sortable对象, 包含所有的函数和参数
			 * el 拖拽的容器 grid-layout
			 * options 传入的所有参数
			 * ownerDocument 当前容器位于的整个文档 document
			 * dragStartFn 初始化的开始拖拽函数
			 * */
			// 拿到拖拽对象的大小及其相对于[视口Viewport]的位置
			let dragRect = getRect(target)
			// 将拖拽的容器赋值给全局变量「rootEl」
			rootEl = el
			// 将拖拽元素dom赋值给全局变量「dragEl」
			dragEl = target
			// 将拖拽元素的父节点赋值给全局变量 「parentEl」
			parentEl = dragEl.parentNode
			// 将拖拽元素的下一个节点赋值给全局变量 「nexEl」
			nextEl = dragEl.nextSibling
			// 最后按下的元素
			lastDownEl = target
			activeGroup = options.group

			// 将正在拖拽的元素赋值给Sortable.dragged变量
			Sortable.dragged = dragEl

			tapEvt = {
				target: dragEl,
				clientX: (touch || evt).clientX,
				clientY: (touch || evt).clientY
			}

			// 获取到相对于拖拽元素点击的位置并且赋值给全局变量 「tapDistanceLeft」
			tapDistanceLeft = tapEvt.clientX - dragRect.left
			tapDistanceTop = tapEvt.clientY - dragRect.top

			const cloneDragEl = clone(dragEl)
			if (!cacheVar.get('dragging')) {
				// console.log('触发一次存储')
				cacheVar.set('rootEl', el)
				cacheVar.set('dragEl', cloneDragEl)
				cacheVar.set('dragEl', dragEl)
				cacheVar.set('parentEl', dragEl.parentNode)
				cacheVar.set('nextEl', dragEl.nextSibling)
				cacheVar.set('lastDownEl', target)
				cacheVar.set('activeGroup', options.group)
				cacheVar.set('tapDistanceLeft', tapDistanceLeft)
				cacheVar.set('tapDistanceTop', tapDistanceTop)
			}

			// 将点击的相对于视窗的位置赋值给当前拖拽构造函数的变量
			this._lastX = (touch || evt).clientX
			this._lastY = (touch || evt).clientY

			dragEl.style['will-change'] = 'all'

			dragStartFn = function () {
				pluginEvent('delayEnded', _this, { evt })
				if (Sortable.eventCanceled) {
					_this._onDrop()
					return
				}
				// Delayed drag has been triggered
				// we can re-enable the events: touchmove/mousemove
				_this._disableDelayedDragEvents()

				if (!FireFox && _this.nativeDraggable) {
					dragEl.draggable = true
				}

				// Bind the events: dragstart/dragend
				// 按下之后绑定事件
				_this._triggerDragStart(evt, touch)

				// Drag start event
				// 触发options中choose事件
				_dispatchEvent({
					sortable: _this,
					name: 'choose',
					originalEvent: evt
				})

				// Chosen item
				// 设置拖拽元素的class
				toggleClass(dragEl, options.chosenClass, true)
			}

			// Disable "draggable"
			options.ignore.split(',').forEach(function (criteria) {
				find(dragEl, criteria.trim(), _disableDraggable)
			})

			// 开始监听这些事件
			on(ownerDocument, 'dragover', nearestEmptyInsertDetectEvent)
			on(ownerDocument, 'mousemove', nearestEmptyInsertDetectEvent)
			on(ownerDocument, 'touchmove', nearestEmptyInsertDetectEvent)

			on(ownerDocument, 'mouseup', _this._onDrop)
			on(ownerDocument, 'touchend', _this._onDrop)
			on(ownerDocument, 'touchcancel', _this._onDrop)

			// Make dragEl draggable (must be before delay for FireFox)
			if (FireFox && this.nativeDraggable) {
				this.options.touchStartThreshold = 4
				dragEl.draggable = true
			}

			pluginEvent('delayStart', this, { evt })

			// Delay is impossible for native DnD in Edge or IE
			if (
				options.delay &&
				(!options.delayOnTouchOnly || touch) &&
				(!this.nativeDraggable || !(Edge || IE11OrLess))
			) {
				if (Sortable.eventCanceled) {
					this._onDrop()
					return
				}
				// If the user moves the pointer or let go the click or touch
				// before the delay has been reached:
				// disable the delayed drag
				on(ownerDocument, 'mouseup', _this._disableDelayedDrag)
				on(ownerDocument, 'touchend', _this._disableDelayedDrag)
				on(ownerDocument, 'touchcancel', _this._disableDelayedDrag)
				on(ownerDocument, 'mousemove', _this._delayedDragTouchMoveHandler)
				on(ownerDocument, 'touchmove', _this._delayedDragTouchMoveHandler)
				options.supportPointer &&
					on(ownerDocument, 'pointermove', _this._delayedDragTouchMoveHandler)

				_this._dragStartTimer = setTimeout(dragStartFn, options.delay)
			} else {
				dragStartFn()
			}
		}
	},

	// _延迟拖动触摸移动处理程序
	_delayedDragTouchMoveHandler: function (/** TouchEvent|PointerEvent **/ e) {
		let touch = e.touches ? e.touches[0] : e
		if (
			Math.max(
				Math.abs(touch.clientX - this._lastX),
				Math.abs(touch.clientY - this._lastY)
			) >=
			Math.floor(
				this.options.touchStartThreshold /
					((this.nativeDraggable && window.devicePixelRatio) || 1)
			)
		) {
			this._disableDelayedDrag()
		}
	},

	_disableDelayedDrag: function () {
		dragEl && _disableDraggable(dragEl)
		clearTimeout(this._dragStartTimer)

		this._disableDelayedDragEvents()
	},

	_disableDelayedDragEvents: function () {
		let ownerDocument = this.el.ownerDocument
		off(ownerDocument, 'mouseup', this._disableDelayedDrag)
		off(ownerDocument, 'touchend', this._disableDelayedDrag)
		off(ownerDocument, 'touchcancel', this._disableDelayedDrag)
		off(ownerDocument, 'mousemove', this._delayedDragTouchMoveHandler)
		off(ownerDocument, 'touchmove', this._delayedDragTouchMoveHandler)
		off(ownerDocument, 'pointermove', this._delayedDragTouchMoveHandler)
	},

	_triggerDragStart: function (/** Event */ evt, /** Touch */ touch) {
		touch = touch || (evt.pointerType == 'touch' && evt)

		// 如果不支持原生拖拽和触摸事件
		if (!this.nativeDraggable || touch) {
			if (this.options.supportPointer) {
				on(document, 'pointermove', this._onTouchMove)
			} else if (touch) {
				on(document, 'touchmove', this._onTouchMove)
			} else {
				on(document, 'mousemove', this._onTouchMove)
			}
		} else {
			on(dragEl, 'dragend', this)
			on(rootEl, 'dragstart', this._onDragStart)
		}

		try {
			if (document.selection) {
				// Timeout neccessary for IE9
				_nextTick(function () {
					document.selection.empty()
				})
			} else {
				window.getSelection().removeAllRanges()
			}
		} catch (err) {}
	},

	_dragStarted: function (fallback, evt) {
		let _this = this
		awaitingDragStarted = false
		if (rootEl && dragEl) {
			pluginEvent('dragStarted', this, { evt })

			if (this.nativeDraggable) {
				on(document, 'dragover', _checkOutsideTargetEl)
			}
			let options = this.options

			// Apply effect
			!fallback && toggleClass(dragEl, options.dragClass, false)
			toggleClass(dragEl, options.ghostClass, true)

			Sortable.active = this

			fallback && this._appendGhost()

			// Drag start event
			_dispatchEvent({
				sortable: this,
				name: 'start',
				originalEvent: evt
			})
		} else {
			this._nulling()
		}
	},

	_emulateDragOver: function () {
		if (touchEvt) {
			this._lastX = touchEvt.clientX
			this._lastY = touchEvt.clientY

			_hideGhostForTarget()

			let target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY)
			let parent = target

			while (target && target.shadowRoot) {
				target = target.shadowRoot.elementFromPoint(
					touchEvt.clientX,
					touchEvt.clientY
				)
				if (target === parent) break
				parent = target
			}

			dragEl.parentNode[expando]._isOutsideThisEl(target)

			if (parent) {
				do {
					if (parent[expando]) {
						let inserted

						inserted = parent[expando]._onDragOver({
							clientX: touchEvt.clientX,
							clientY: touchEvt.clientY,
							target: target,
							rootEl: parent
						})

						if (inserted && !this.options.dragoverBubble) {
							break
						}
					}

					target = parent // store last element
				} while (
					/* jshint boss:true */
					(parent = parent.parentNode)
				)
			}

			_unhideGhostForTarget()
		}
	},

	_onTouchMove: function (/**TouchEvent*/ evt) {
		if (tapEvt) {
			let options = this.options,
				fallbackTolerance = options.fallbackTolerance,
				fallbackOffset = options.fallbackOffset,
				touch = evt.touches ? evt.touches[0] : evt,
				ghostMatrix = ghostEl && matrix(ghostEl, true),
				scaleX = ghostEl && ghostMatrix && ghostMatrix.a,
				scaleY = ghostEl && ghostMatrix && ghostMatrix.d,
				relativeScrollOffset =
					PositionGhostAbsolutely &&
					ghostRelativeParent &&
					getRelativeScrollOffset(ghostRelativeParent),
				dx =
					(touch.clientX - tapEvt.clientX + fallbackOffset.x) / (scaleX || 1) +
					(relativeScrollOffset
						? relativeScrollOffset[0] - ghostRelativeParentInitialScroll[0]
						: 0) /
						(scaleX || 1),
				dy =
					(touch.clientY - tapEvt.clientY + fallbackOffset.y) / (scaleY || 1) +
					(relativeScrollOffset
						? relativeScrollOffset[1] - ghostRelativeParentInitialScroll[1]
						: 0) /
						(scaleY || 1)

			// only set the status to dragging, when we are actually dragging
			if (!Sortable.active && !awaitingDragStarted) {
				if (
					fallbackTolerance &&
					Math.max(
						Math.abs(touch.clientX - this._lastX),
						Math.abs(touch.clientY - this._lastY)
					) < fallbackTolerance
				) {
					return
				}
				this._onDragStart(evt, true)
			}

			if (ghostEl) {
				if (ghostMatrix) {
					ghostMatrix.e += dx - (lastDx || 0)
					ghostMatrix.f += dy - (lastDy || 0)
				} else {
					ghostMatrix = {
						a: 1,
						b: 0,
						c: 0,
						d: 1,
						e: dx,
						f: dy
					}
				}

				let cssMatrix = `matrix(${ghostMatrix.a},${ghostMatrix.b},${ghostMatrix.c},${ghostMatrix.d},${ghostMatrix.e},${ghostMatrix.f})`

				css(ghostEl, 'webkitTransform', cssMatrix)
				css(ghostEl, 'mozTransform', cssMatrix)
				css(ghostEl, 'msTransform', cssMatrix)
				css(ghostEl, 'transform', cssMatrix)

				lastDx = dx
				lastDy = dy

				touchEvt = touch
			}

			evt.cancelable && evt.preventDefault()
		}
	},

	_appendGhost: function () {
		// Bug if using scale(): https://stackoverflow.com/questions/2637058
		// Not being adjusted for
		if (!ghostEl) {
			let container = this.options.fallbackOnBody ? document.body : rootEl,
				rect = getRect(dragEl, true, PositionGhostAbsolutely, true, container),
				options = this.options

			// Position absolutely
			if (PositionGhostAbsolutely) {
				// Get relatively positioned parent
				ghostRelativeParent = container

				while (
					css(ghostRelativeParent, 'position') === 'static' &&
					css(ghostRelativeParent, 'transform') === 'none' &&
					ghostRelativeParent !== document
				) {
					ghostRelativeParent = ghostRelativeParent.parentNode
				}

				if (
					ghostRelativeParent !== document.body &&
					ghostRelativeParent !== document.documentElement
				) {
					if (ghostRelativeParent === document)
						ghostRelativeParent = getWindowScrollingElement()

					rect.top += ghostRelativeParent.scrollTop
					rect.left += ghostRelativeParent.scrollLeft
				} else {
					ghostRelativeParent = getWindowScrollingElement()
				}
				ghostRelativeParentInitialScroll =
					getRelativeScrollOffset(ghostRelativeParent)
			}

			ghostEl = dragEl.cloneNode(true)

			toggleClass(ghostEl, options.ghostClass, false)
			toggleClass(ghostEl, options.fallbackClass, true)
			toggleClass(ghostEl, options.dragClass, true)

			css(ghostEl, 'transition', '')
			css(ghostEl, 'transform', '')

			css(ghostEl, 'box-sizing', 'border-box')
			css(ghostEl, 'margin', 0)
			css(ghostEl, 'top', rect.top)
			css(ghostEl, 'left', rect.left)
			css(ghostEl, 'width', rect.width)
			css(ghostEl, 'height', rect.height)
			css(ghostEl, 'opacity', '0.8')
			css(ghostEl, 'position', PositionGhostAbsolutely ? 'absolute' : 'fixed')
			css(ghostEl, 'zIndex', '100000')
			css(ghostEl, 'pointerEvents', 'none')

			Sortable.ghost = ghostEl

			container.appendChild(ghostEl)

			// Set transform-origin
			css(
				ghostEl,
				'transform-origin',
				(tapDistanceLeft / parseInt(ghostEl.style.width)) * 100 +
					'% ' +
					(tapDistanceTop / parseInt(ghostEl.style.height)) * 100 +
					'%'
			)
		}
	},

	_onDragStart: function (/**Event*/ evt, /**boolean*/ fallback) {
		let _this = this
		let dataTransfer = evt.dataTransfer
		let options = _this.options

		pluginEvent('dragStart', this, { evt })
		if (Sortable.eventCanceled) {
			this._onDrop()
			return
		}

		pluginEvent('setupClone', this)
		if (!Sortable.eventCanceled) {
			// clone 一个 dragEl 元素副本，用于两个容器项目移动时使用；
			cloneEl = clone(dragEl)
			cloneEl.removeAttribute('id')
			cloneEl.draggable = false
			cloneEl.style['will-change'] = ''

			this._hideClone()

			toggleClass(cloneEl, this.options.chosenClass, false)
			Sortable.clone = cloneEl
		}

		// #1143: IFrame support workaround
		_this.cloneId = _nextTick(function () {
			pluginEvent('clone', _this)
			if (Sortable.eventCanceled) return

			if (!_this.options.removeCloneOnHide) {
				rootEl.insertBefore(cloneEl, dragEl)
			}
			_this._hideClone()

			// 触发外部传入的 clone 和 dragstart 事件；
			_dispatchEvent({
				sortable: _this,
				name: 'clone'
			})
		})

		!fallback && toggleClass(dragEl, options.dragClass, true)

		// Set proper drop events
		if (fallback) {
			ignoreNextClick = true
			_this._loopId = setInterval(_this._emulateDragOver, 50)
		} else {
			// Undo what was set in _prepareDragStart before drag started
			off(document, 'mouseup', _this._onDrop)
			off(document, 'touchend', _this._onDrop)
			off(document, 'touchcancel', _this._onDrop)

			// 设置拖拽数据
			if (dataTransfer) {
				dataTransfer.effectAllowed = 'move'
				options.setData && options.setData.call(_this, dataTransfer, dragEl)
			}

			on(document, 'drop', _this)

			// #1276 fix:
			css(dragEl, 'transform', 'translateZ(0)')
		}

		awaitingDragStarted = true

		_this._dragStartId = _nextTick(
			_this._dragStarted.bind(_this, fallback, evt)
		)
		on(document, 'selectstart', _this)

		moved = true

		if (Safari) {
			css(document.body, 'user-select', 'none')
		}
	},

	// Returns true - if no further action is needed (either inserted or another condition)
	// 返回 true - 如果不需要进一步操作（插入或其他条件）
	_onDragOver: function (/**Event*/ evt) {
		const currentTime = Date.now()
		const elapsedTime = currentTime - lastExecutionTime
		if (elapsedTime <= MAX_TRIGGER_TIME) return
		lastExecutionTime = currentTime
		if (!cacheVar.get('dragging')) {
			cacheVar.set('dragging', true)
		}
		// cacheVar.set('dragging', true);
		let el = this.el,
			target = evt.target,
			dragRect,
			targetRect,
			revert,
			options = this.options,
			group = options.group,
			activeSortable = Sortable.active,
			isOwner = activeGroup === group,
			canSort = options.sort,
			fromSortable = putSortable || activeSortable,
			vertical,
			_this = this,
			completedFired = false,
			prevElement

		if (_silent) return

		function dragOverEvent(name, extra) {
			pluginEvent(name, _this, {
				evt,
				isOwner,
				axis: vertical ? 'vertical' : 'horizontal',
				revert,
				dragRect,
				targetRect,
				canSort,
				fromSortable,
				target,
				completed,
				onMove(target, after) {
					return onMove(
						rootEl,
						el,
						dragEl,
						dragRect,
						target,
						getRect(target),
						evt,
						after
					)
				},
				changed,
				...extra
			})
		}

		// Capture animation state
		// 捕获动画状态
		function capture() {
			dragOverEvent('dragOverAnimationCapture')

			_this.captureAnimationState()
			if (_this !== fromSortable) {
				fromSortable.captureAnimationState()
			}
		}

		// Return invocation when dragEl is inserted (or completed)
		// 插入（或完成）dragEl 时返回调用
		function completed(insertion) {
			dragOverEvent('dragOverCompleted', { insertion })

			if (insertion) {
				// Clones must be hidden before folding animation to capture dragRectAbsolute properly
				// 必须在折叠动画之前隐藏克隆才能正确捕获 DragRectAbsolute
				if (isOwner) {
					activeSortable._hideClone()
				} else {
					activeSortable._showClone(_this)
				}

				if (_this !== fromSortable) {
					// Set ghost class to new sortable's ghost class
					toggleClass(
						dragEl,
						putSortable
							? putSortable.options.ghostClass
							: activeSortable.options.ghostClass,
						false
					)
					toggleClass(dragEl, options.ghostClass, true)
				}

				if (putSortable !== _this && _this !== Sortable.active) {
					putSortable = _this
				} else if (_this === Sortable.active && putSortable) {
					putSortable = null
				}

				// Animation
				if (fromSortable === _this) {
					_this._ignoreWhileAnimating = target
				}
				_this.animateAll(function () {
					dragOverEvent('dragOverAnimationComplete')
					_this._ignoreWhileAnimating = null
				})
				if (_this !== fromSortable) {
					fromSortable.animateAll()
					fromSortable._ignoreWhileAnimating = null
				}
			}

			// Null lastTarget if it is not inside a previously swapped element
			if (
				(target === dragEl && !dragEl.animated) ||
				(target === el && !target.animated)
			) {
				lastTarget = null
			}

			// no bubbling and not fallback
			if (!options.dragoverBubble && !evt.rootEl && target !== document) {
				// console.log('evt', evt, dragEl)
				if (dragEl) {
					dragEl.parentNode[expando]._isOutsideThisEl(evt.target)
				}
				// Do not detect for empty insert if already inserted
				!insertion && nearestEmptyInsertDetectEvent(evt)
			}

			!options.dragoverBubble && evt.stopPropagation && evt.stopPropagation()

			return (completedFired = true)
		}

		// Call when dragEl has been inserted
		// 当dragEl被插入时调用
		function changed() {
			newIndex = index(dragEl)
			newDraggableIndex = index(dragEl, options.draggable)

			// console.log('newIndex, oldIndex changed', newIndex, oldIndex)

			_dispatchEvent({
				sortable: _this,
				name: 'change',
				toEl: el,
				newIndex,
				newDraggableIndex,
				originalEvent: evt
			})
		}

		if (evt.preventDefault !== void 0) {
			evt.cancelable && evt.preventDefault()
		}

		target = closest(target, options.draggable, el, true)

		dragOverEvent('dragOver')
		if (Sortable.eventCanceled) return completedFired

		if (
			dragEl.contains(evt.target) ||
			(target.animated && target.animatingX && target.animatingY) ||
			_this._ignoreWhileAnimating === target
		) {
			return completed(false)
		}

		ignoreNextClick = false

		if (
			activeSortable &&
			!options.disabled &&
			(isOwner
				? canSort || (revert = parentEl !== rootEl) // Reverting item into the original list
				: putSortable === this ||
				  ((this.lastPutMode = activeGroup.checkPull(
						this,
						activeSortable,
						dragEl,
						evt
				  )) &&
						group.checkPut(this, activeSortable, dragEl, evt)))
		) {
			vertical = this._getDirection(evt, target) === 'vertical'

			// 获取拖动元素的坐标信息
			dragRect = getRect(dragEl)

			dragOverEvent('dragOverValid')
			if (Sortable.eventCanceled) return completedFired

			if (revert) {
				parentEl = rootEl // actualization
				capture()

				this._hideClone()

				dragOverEvent('revert')

				if (!Sortable.eventCanceled) {
					if (nextEl) {
						rootEl.insertBefore(dragEl, nextEl)
					} else {
						rootEl.appendChild(dragEl)
					}
				}

				return completed(true)
			}

			let elLastChild = lastChild(el, options.draggable)

			if (
				!elLastChild ||
				(_ghostIsLast(evt, vertical, this) && !elLastChild.animated)
			) {
				// Insert to end of list

				// If already at end of list: Do not insert
				if (elLastChild === dragEl) {
					return completed(false)
				}

				// if there is a last element, it is the target
				if (elLastChild && el === evt.target) {
					target = elLastChild
				}

				if (target) {
					targetRect = getRect(target)
				}
			}

			if (target === dragEl.parentNode) {
				return completed(false)
			}
			// -----------------------------自定义------------------------------------
			let dndDirection = getDirection(dragEl, target)
			// console.log('dndDirection', dndDirection)
			let activeSortable = Sortable.active

			if (dndDirection !== 'horizontal') {
				parentEl = dragEl.parentNode // actualization

				_silent = true
				setTimeout(_unsilent, 30)

				capture()
				// console.log('dragEl', dragEl, target)
				// 是否开启交换而不是插入
				if (activeSortable.options.openSwap) {
					//
					let prevSwapEl = lastSwapEl
					if (
						onMove(
							rootEl,
							el,
							dragEl,
							dragRect,
							target,
							targetRect,
							evt,
							false
						) !== false
					) {
						toggleClass(target, options.customSwapClass, true)
						lastSwapEl = target
					} else {
						lastSwapEl = null
					}

					if (prevSwapEl && prevSwapEl !== lastSwapEl) {
						toggleClass(prevSwapEl, options.customSwapClass, false)
					}

					// 缓存的还是之前的DOM, 但是进入非自己的元素之后坐标位置会变, 需要根据索引去获取到最新的DOM
					// 主项目内数据没变, 所以去同步位置不会生效

					// 跨大行拖拽
					if (dndDirection === 'vertical-across-rows') {
						const dragPos = getPosition(dragEl)
						const targetPos = getPosition(target)
						if (dragPos && targetPos) {
							if (dragPos.length === targetPos.length) {
								dndDirection = 'vertical'
							} else {
								const allDoms = fillArray(dragPos, targetPos, dragEl, target)
								// console.log('allDoms', allDoms)
								_dispatchEvent({
									sortable: this,
									name: 'customSwap',
									toEl: parentEl,
									originalEvent: evt,
									swapDoms: allDoms.doms,
									maxDom: allDoms.maxPosDom,
									indexes: allDoms.indexes
								})
								setTimeout(() => {
									changed()
									return completed(true)
								}, 0)
							}
						} else {
							setTimeout(() => {
								changed()
								return completed(true)
							}, 0)
						}
					}
					// 小图标上下垂直拖拽
					if (dndDirection === 'vertical') {
						newIndex = index(dragEl)
						newDraggableIndex = index(dragEl, options.draggable)
						oldIndex = index(target)
						oldDraggableIndex = index(target, options.draggable)
						if (newIndex !== oldIndex) {
							if (newIndex >= 0) {
								// drag & drop within the same list
								_dispatchEvent({
									sortable: this,
									name: 'update',
									toEl: parentEl,
									originalEvent: evt
								})
								_dispatchEvent({
									sortable: this,
									name: 'sort',
									toEl: parentEl,
									originalEvent: evt
								})
							}
							if (prevElement) {
								dragEl = prevElement
								prevElement = null
							}
							this.save()
						}

						setTimeout(() => {
							changed()
							return completed(true)
						}, 0)
					}
					// 拖拽奇数行小组件放到偶数行中/大组件
					if (dndDirection === 'vertical-drag') {
						prevElement = dragEl
						const evenElement = getElement(dragEl)
						if (evenElement) {
							dragEl = evenElement
							oldIndex = index(evenElement)
							oldDraggableIndex = index(evenElement, options.draggable)
							newIndex = index(target)
							newDraggableIndex = index(target, options.draggable)
						}
						if (newIndex !== oldIndex) {
							if (newIndex >= 0) {
								// drag & drop within the same list
								_dispatchEvent({
									sortable: this,
									name: 'update',
									toEl: parentEl,
									originalEvent: evt
								})
								_dispatchEvent({
									sortable: this,
									name: 'sort',
									toEl: parentEl,
									originalEvent: evt
								})
							}
							if (prevElement) {
								dragEl = prevElement
								prevElement = null
							}
							this.save()
						}

						setTimeout(() => {
							changed()
							return completed(true)
						}, 0)
					}
					// 拖拽中/大组件放到奇数行小组件
					if (dndDirection === 'vertical-drop') {
						prevElement = target
						const evenElement = getElement(target)
						if (evenElement) {
							target = evenElement
							newIndex = index(evenElement)
							newDraggableIndex = index(evenElement, options.draggable)
							oldIndex = index(dragEl)
							oldDraggableIndex = index(dragEl, options.draggable)
						}
						if (newIndex !== oldIndex) {
							if (newIndex >= 0) {
								// drag & drop within the same list
								_dispatchEvent({
									sortable: this,
									name: 'update',
									toEl: parentEl,
									originalEvent: evt
								})
								_dispatchEvent({
									sortable: this,
									name: 'sort',
									toEl: parentEl,
									originalEvent: evt
								})
							}
							if (prevElement) {
								target = prevElement
								prevElement = null
							}
							this.save()
						}

						setTimeout(() => {
							changed()
							return completed(true)
						}, 0)
					}
				}
			} else {
				parentEl = dragEl.parentNode // actualization

				_silent = true
				setTimeout(_unsilent, 30)

				capture()
				oldIndex = index(target)
				oldDraggableIndex = index(target, options.draggable)

				newIndex = index(dragEl)
				newDraggableIndex = index(dragEl, options.draggable)
				if (newIndex !== oldIndex) {
					if (newIndex >= 0) {
						// drag & drop within the same list
						_dispatchEvent({
							sortable: this,
							name: 'update',
							toEl: parentEl,
							originalEvent: evt
						})
						_dispatchEvent({
							sortable: this,
							name: 'sort',
							toEl: parentEl,
							originalEvent: evt
						})
					}
					this.save()
				}
				setTimeout(() => {
					changed()
					return completed(true)
				}, 0)
			}

			if (el.contains(dragEl)) {
				return completed(false)
			}
		}

		return false
	},

	_ignoreWhileAnimating: null,

	_offMoveEvents: function () {
		off(document, 'mousemove', this._onTouchMove)
		off(document, 'touchmove', this._onTouchMove)
		off(document, 'pointermove', this._onTouchMove)
		off(document, 'dragover', nearestEmptyInsertDetectEvent)
		off(document, 'mousemove', nearestEmptyInsertDetectEvent)
		off(document, 'touchmove', nearestEmptyInsertDetectEvent)
	},

	_offUpEvents: function () {
		let ownerDocument = this.el.ownerDocument

		off(ownerDocument, 'mouseup', this._onDrop)
		off(ownerDocument, 'touchend', this._onDrop)
		off(ownerDocument, 'pointerup', this._onDrop)
		off(ownerDocument, 'touchcancel', this._onDrop)
		off(document, 'selectstart', this)
	},

	_onDrop: function (/**Event*/ evt) {
		// console.log('松手了')

		let el = this.el,
			options = this.options
		// Get the index of the dragged element within its parent
		newIndex = index(dragEl)
		newDraggableIndex = index(dragEl, options.draggable)

		pluginEvent('drop', this, {
			evt
		})

		parentEl = dragEl && dragEl.parentNode

		// Get again after plugin event
		newIndex = index(dragEl)
		newDraggableIndex = index(dragEl, options.draggable)

		if (Sortable.eventCanceled) {
			this._nulling()
			return
		}

		awaitingDragStarted = false
		isCircumstantialInvert = false
		pastFirstInvertThresh = false

		clearInterval(this._loopId)

		clearTimeout(this._dragStartTimer)

		_cancelNextTick(this.cloneId)
		_cancelNextTick(this._dragStartId)

		// Unbind events
		if (this.nativeDraggable) {
			off(document, 'drop', this)
			off(el, 'dragstart', this._onDragStart)
		}
		this._offMoveEvents()
		this._offUpEvents()

		if (Safari) {
			css(document.body, 'user-select', '')
		}

		css(dragEl, 'transform', '')

		if (evt) {
			if (moved) {
				evt.cancelable && evt.preventDefault()
				!options.dropBubble && evt.stopPropagation()
			}

			ghostEl && ghostEl.parentNode && ghostEl.parentNode.removeChild(ghostEl)

			if (
				rootEl === parentEl ||
				(putSortable && putSortable.lastPutMode !== 'clone')
			) {
				// Remove clone(s)
				cloneEl && cloneEl.parentNode && cloneEl.parentNode.removeChild(cloneEl)
			}

			if (dragEl) {
				if (this.nativeDraggable) {
					off(dragEl, 'dragend', this)
				}

				_disableDraggable(dragEl)
				dragEl.style['will-change'] = ''

				// Remove classes
				// ghostClass is added in dragStarted
				if (moved && !awaitingDragStarted) {
					toggleClass(
						dragEl,
						putSortable
							? putSortable.options.ghostClass
							: this.options.ghostClass,
						false
					)
				}
				toggleClass(dragEl, this.options.chosenClass, false)

				// Drag stop event
				// 元素取消选中
				_dispatchEvent({
					sortable: this,
					name: 'unchoose',
					toEl: parentEl,
					newIndex: null,
					newDraggableIndex: null,
					originalEvent: evt
				})

				if (rootEl !== parentEl) {
					if (newIndex >= 0) {
						// Add event
						// 元素从一个列表拖拽到另一个列表
						_dispatchEvent({
							rootEl: parentEl,
							name: 'add',
							toEl: parentEl,
							fromEl: rootEl,
							originalEvent: evt
						})

						// Remove event
						// 元素从列表中移除进入另一个列表
						_dispatchEvent({
							sortable: this,
							name: 'remove',
							toEl: parentEl,
							originalEvent: evt
						})

						// drag from one list and drop into another
						_dispatchEvent({
							rootEl: parentEl,
							name: 'sort',
							toEl: parentEl,
							fromEl: rootEl,
							originalEvent: evt
						})

						_dispatchEvent({
							sortable: this,
							name: 'sort',
							toEl: parentEl,
							originalEvent: evt
						})
					}

					putSortable && putSortable.save()
				} else {
					// if (newIndex !== oldIndex) {
					// 	if (newIndex >= 0) {
					// 		// drag & drop within the same list
					// 		console.log('newIndex, oldIndex update', newIndex, oldIndex)
					// 		// _dispatchEvent({
					// 		// 	sortable: this,
					// 		// 	name: 'update',
					// 		// 	toEl: parentEl,
					// 		// 	originalEvent: evt
					// 		// })
					// 		_dispatchEvent({
					// 			sortable: this,
					// 			name: 'sort',
					// 			toEl: parentEl,
					// 			originalEvent: evt
					// 		})
					// 	}
					// }
				}

				if (Sortable.active) {
					/* jshint eqnull:true */
					if (newIndex == null || newIndex === -1) {
						newIndex = oldIndex
						newDraggableIndex = oldDraggableIndex
					}

					_dispatchEvent({
						sortable: this,
						name: 'end',
						toEl: parentEl,
						originalEvent: evt
					})

					// Save sorting
					this.save()
				}
			}
		}
		toggleClass(lastSwapEl, options.customSwapClass, false)
		this._nulling()
	},

	_nulling: function () {
		pluginEvent('nulling', this)
		cacheVar.clear()
		lastSwapEl = null

		rootEl =
			dragEl =
			parentEl =
			ghostEl =
			nextEl =
			cloneEl =
			lastDownEl =
			cloneHidden =
			tapEvt =
			touchEvt =
			moved =
			newIndex =
			newDraggableIndex =
			oldIndex =
			oldDraggableIndex =
			lastTarget =
			lastDirection =
			putSortable =
			activeGroup =
			Sortable.dragged =
			Sortable.ghost =
			Sortable.clone =
			Sortable.active =
				null

		savedInputChecked.forEach(function (el) {
			el.checked = true
		})

		savedInputChecked.length = lastDx = lastDy = 0
	},

	handleEvent: function (/**Event*/ evt) {
		switch (evt.type) {
			case 'drop':
			case 'dragend':
				this._onDrop(evt)
				break

			case 'dragenter':
			case 'dragover':
				if (dragEl) {
					this._onDragOver(evt)
					_globalDragOver(evt)
				}
				break

			case 'selectstart':
				evt.preventDefault()
				break
		}
	},

	/**
	 * Serializes the item into an array of string.
	 * @returns {String[]}
	 */
	toArray: function () {
		let order = [],
			el,
			children = this.el.children,
			i = 0,
			n = children.length,
			options = this.options

		for (; i < n; i++) {
			el = children[i]
			if (closest(el, options.draggable, this.el, false)) {
				order.push(el.getAttribute(options.dataIdAttr) || _generateId(el))
			}
		}

		return order
	},

	/**
	 * Sorts the elements according to the array.
	 * @param  {String[]}  order  order of the items
	 */
	sort: function (order, useAnimation) {
		let items = {},
			rootEl = this.el

		this.toArray().forEach(function (id, i) {
			let el = rootEl.children[i]

			if (closest(el, this.options.draggable, rootEl, false)) {
				items[id] = el
			}
		}, this)

		useAnimation && this.captureAnimationState()
		order.forEach(function (id) {
			if (items[id]) {
				rootEl.removeChild(items[id])
				rootEl.appendChild(items[id])
			}
		})
		useAnimation && this.animateAll()
	},

	/**
	 * Save the current sorting
	 */
	save: function () {
		let store = this.options.store
		store && store.set && store.set(this)
	},

	/**
	 * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
	 * @param   {HTMLElement}  el
	 * @param   {String}       [selector]  default: `options.draggable`
	 * @returns {HTMLElement|null}
	 */
	closest: function (el, selector) {
		return closest(el, selector || this.options.draggable, this.el, false)
	},

	/**
	 * Set/get option
	 * @param   {string} name
	 * @param   {*}      [value]
	 * @returns {*}
	 */
	option: function (name, value) {
		let options = this.options

		if (value === void 0) {
			return options[name]
		} else {
			let modifiedValue = PluginManager.modifyOption(this, name, value)
			if (typeof modifiedValue !== 'undefined') {
				options[name] = modifiedValue
			} else {
				options[name] = value
			}

			if (name === 'group') {
				_prepareGroup(options)
			}
		}
	},

	/**
	 * Destroy
	 */
	destroy: function () {
		pluginEvent('destroy', this)
		let el = this.el

		el[expando] = null

		off(el, 'mousedown', this._onTapStart)
		off(el, 'touchstart', this._onTapStart)
		off(el, 'pointerdown', this._onTapStart)

		if (this.nativeDraggable) {
			off(el, 'dragover', this)
			off(el, 'dragenter', this)
		}
		// Remove draggable attributes
		Array.prototype.forEach.call(
			el.querySelectorAll('[draggable]'),
			function (el) {
				el.removeAttribute('draggable')
			}
		)

		this._onDrop()

		this._disableDelayedDragEvents()

		sortables.splice(sortables.indexOf(this.el), 1)

		this.el = el = null
	},

	_hideClone: function () {
		if (!cloneHidden) {
			pluginEvent('hideClone', this)
			if (Sortable.eventCanceled) return

			css(cloneEl, 'display', 'none')
			if (this.options.removeCloneOnHide && cloneEl && cloneEl.parentNode) {
				cloneEl.parentNode.removeChild(cloneEl)
			}
			cloneHidden = true
		}
	},

	_showClone: function (putSortable) {
		if (putSortable.lastPutMode !== 'clone') {
			this._hideClone()
			return
		}

		if (cloneHidden) {
			pluginEvent('showClone', this)
			if (Sortable.eventCanceled) return

			// show clone at dragEl or original position
			if (dragEl.parentNode == rootEl && !this.options.group.revertClone) {
				rootEl.insertBefore(cloneEl, dragEl)
			} else if (nextEl) {
				rootEl.insertBefore(cloneEl, nextEl)
			} else {
				rootEl.appendChild(cloneEl)
			}
			if (this.options.group.revertClone) {
				this.animate(dragEl, cloneEl)
			}

			css(cloneEl, 'display', '')
			cloneHidden = false
		}
	}
}

function _globalDragOver(/**Event*/ evt) {
	if (evt.dataTransfer) {
		evt.dataTransfer.dropEffect = 'move'
	}
	evt.cancelable && evt.preventDefault()
}

function onMove(
	fromEl,
	toEl,
	dragEl,
	dragRect,
	targetEl,
	targetRect,
	originalEvent,
	willInsertAfter
) {
	let evt,
		sortable = fromEl[expando],
		onMoveFn = sortable.options.onMove,
		retVal
	// Support for new CustomEvent feature
	if (window.CustomEvent && !IE11OrLess && !Edge) {
		evt = new CustomEvent('move', {
			bubbles: true,
			cancelable: true
		})
	} else {
		evt = document.createEvent('Event')
		evt.initEvent('move', true, true)
	}

	evt.to = toEl
	evt.from = fromEl
	evt.dragged = dragEl
	evt.draggedRect = dragRect
	evt.related = targetEl || toEl
	evt.relatedRect = targetRect || getRect(toEl)
	evt.willInsertAfter = willInsertAfter

	evt.originalEvent = originalEvent

	fromEl.dispatchEvent(evt)

	if (onMoveFn) {
		retVal = onMoveFn.call(sortable, evt, originalEvent)
	}

	return retVal
}

function _disableDraggable(el) {
	el.draggable = false
}

function _unsilent() {
	_silent = false
}

function _ghostIsFirst(evt, vertical, sortable) {
	let rect = getRect(getChild(sortable.el, 0, sortable.options, true))
	const spacer = 10

	return vertical
		? evt.clientX < rect.left - spacer ||
				(evt.clientY < rect.top && evt.clientX < rect.right)
		: evt.clientY < rect.top - spacer ||
				(evt.clientY < rect.bottom && evt.clientX < rect.left)
}

function _ghostIsLast(evt, vertical, sortable) {
	let rect = getRect(lastChild(sortable.el, sortable.options.draggable))
	const spacer = 10

	return vertical
		? evt.clientX > rect.right + spacer ||
				(evt.clientX <= rect.right &&
					evt.clientY > rect.bottom &&
					evt.clientX >= rect.left)
		: (evt.clientX > rect.right && evt.clientY > rect.top) ||
				(evt.clientX <= rect.right && evt.clientY > rect.bottom + spacer)
}

function _getSwapDirection(
	evt,
	target,
	targetRect,
	vertical,
	swapThreshold,
	invertedSwapThreshold,
	invertSwap,
	isLastTarget
) {
	let mouseOnAxis = vertical ? evt.clientY : evt.clientX,
		targetLength = vertical ? targetRect.height : targetRect.width,
		targetS1 = vertical ? targetRect.top : targetRect.left,
		targetS2 = vertical ? targetRect.bottom : targetRect.right,
		invert = false

	if (!invertSwap) {
		// Never invert or create dragEl shadow when target movemenet causes mouse to move past the end of regular swapThreshold
		if (isLastTarget && targetMoveDistance < targetLength * swapThreshold) {
			// multiplied only by swapThreshold because mouse will already be inside target by (1 - threshold) * targetLength / 2
			// check if past first invert threshold on side opposite of lastDirection
			if (
				!pastFirstInvertThresh &&
				(lastDirection === 1
					? mouseOnAxis > targetS1 + (targetLength * invertedSwapThreshold) / 2
					: mouseOnAxis < targetS2 - (targetLength * invertedSwapThreshold) / 2)
			) {
				// past first invert threshold, do not restrict inverted threshold to dragEl shadow
				pastFirstInvertThresh = true
			}

			if (!pastFirstInvertThresh) {
				// dragEl shadow (target move distance shadow)
				if (
					lastDirection === 1
						? mouseOnAxis < targetS1 + targetMoveDistance // over dragEl shadow
						: mouseOnAxis > targetS2 - targetMoveDistance
				) {
					return -lastDirection
				}
			} else {
				invert = true
			}
		} else {
			// Regular
			if (
				mouseOnAxis > targetS1 + (targetLength * (1 - swapThreshold)) / 2 &&
				mouseOnAxis < targetS2 - (targetLength * (1 - swapThreshold)) / 2
			) {
				return _getInsertDirection(target)
			}
		}
	}

	invert = invert || invertSwap

	if (invert) {
		// Invert of regular
		if (
			mouseOnAxis < targetS1 + (targetLength * invertedSwapThreshold) / 2 ||
			mouseOnAxis > targetS2 - (targetLength * invertedSwapThreshold) / 2
		) {
			return mouseOnAxis > targetS1 + targetLength / 2 ? 1 : -1
		}
	}

	return 0
}

/**
 * Gets the direction dragEl must be swapped relative to target in order to make it
 * seem that dragEl has been "inserted" into that element's position
 * @param  {HTMLElement} target       The target whose position dragEl is being inserted at
 * @return {Number}                   Direction dragEl must be swapped
 */
function _getInsertDirection(target) {
	if (index(dragEl) < index(target)) {
		return 1
	} else {
		return -1
	}
}

/**
 * Generate id
 * @param   {HTMLElement} el
 * @returns {String}
 * @private
 */
function _generateId(el) {
	let str = el.tagName + el.className + el.src + el.href + el.textContent,
		i = str.length,
		sum = 0

	while (i--) {
		sum += str.charCodeAt(i)
	}

	return sum.toString(36)
}

function _saveInputCheckedState(root) {
	savedInputChecked.length = 0

	let inputs = root.getElementsByTagName('input')
	let idx = inputs.length

	while (idx--) {
		let el = inputs[idx]
		el.checked && savedInputChecked.push(el)
	}
}

function _nextTick(fn) {
	return setTimeout(fn, 0)
}

function _cancelNextTick(id) {
	return clearTimeout(id)
}

// Fixed #973:
if (documentExists) {
	on(document, 'touchmove', function (evt) {
		if ((Sortable.active || awaitingDragStarted) && evt.cancelable) {
			evt.preventDefault()
		}
	})
}

// Export utils
Sortable.utils = {
	on: on,
	off: off,
	css: css,
	find: find,
	is: function (el, selector) {
		return !!closest(el, selector, el, false)
	},
	extend: extend,
	throttle: throttle,
	closest: closest,
	toggleClass: toggleClass,
	clone: clone,
	index: index,
	nextTick: _nextTick,
	cancelNextTick: _cancelNextTick,
	detectDirection: _detectDirection,
	getChild: getChild
}

/**
 * Get the Sortable instance of an element
 * @param  {HTMLElement} element The element
 * @return {Sortable|undefined}         The instance of Sortable
 */
Sortable.get = function (element) {
	return element[expando]
}

/**
 * Mount a plugin to Sortable
 * @param  {...SortablePlugin|SortablePlugin[]} plugins       Plugins being mounted
 */
Sortable.mount = function (...plugins) {
	if (plugins[0].constructor === Array) plugins = plugins[0]

	plugins.forEach(plugin => {
		if (!plugin.prototype || !plugin.prototype.constructor) {
			throw `Sortable: Mounted plugin must be a constructor function, not ${{}.toString.call(
				plugin
			)}`
		}
		if (plugin.utils) Sortable.utils = { ...Sortable.utils, ...plugin.utils }

		PluginManager.mount(plugin)
	})
}

/**
 * Create sortable instance
 * @param {HTMLElement}  el
 * @param {Object}      [options]
 */
Sortable.create = function (el, options) {
	return new Sortable(el, options)
}

// Export
Sortable.version = version

Sortable.mount(SwapPlugin)

export default Sortable
